// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Text;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.Threading;
using Microsoft.TeamFoundation.VersionControl.Common;
using System.Globalization;
using System.Configuration;
using System.Diagnostics;
using Microsoft.TeamFoundation.Migration.Toolkit.VC;

namespace Microsoft.TeamFoundation.Migration.Toolkit
{
    /// <summary>
    /// The BatchingContext defines a method of performing operations against a TFS server
    /// in batches instead of as single operations.  This reduces round-trips with the server
    /// for large changesets.
    /// 
    /// This class takes care of both the batching and also ensuring that the item exists in the
    /// workspace prior to pending the change.  For this reason it is not necessary to "Get" an 
    /// item before acting on it.  This class does not perform a "Get" though as item download is
    /// unnecessary since the item is coming from the source system during migration.
    /// </summary>
    public sealed class BatchingContext
    {
        /// <summary>
        /// Creates a batching context associated with the provided workspace.
        /// </summary>
        /// <param name="workspace"></param>
        public BatchingContext(Workspace workspace)
        {
            if (workspace == null)
            {
                throw new ArgumentNullException("workspace");
            }

            // the default batch size of 200 is the TFS server team recomendation
            m_batchSize = MigrationConfiguration.Current.GetValue<int>(
                Constants.TfsBatchSizeVariableName, 200);

            m_retryLimit = MigrationConfiguration.Current.GetValue<int>("RetryLimit", 10);
            m_secondsToWait = MigrationConfiguration.Current.GetValue<int>("RetryDelaySeconds", 30);
            if (m_secondsToWait < 1)
            {
                m_secondsToWait = 1;
            }


            m_workspace = workspace;
        }

        /// <summary>
        /// Event fired when an error occurs while processing a single item (e.g. add, edit, delete)
        /// </summary>
        public event EventHandler<BatchedItemEventArgs> BatchedItemError;
        
        /// <summary>
        /// Event fired when a warning occurs while processing a single item (e.g. add/undelete 
        /// existing item, delete non existing item)
        /// </summary>
        public event EventHandler<BatchedItemEventArgs> BatchedItemWarning;

        /// <summary>
        /// Event fired when an error occurs while processing a merge action (e.g. merge)
        /// </summary>
        public event EventHandler<BatchedMergeErrorEventArgs> MergeError;

        /// <summary>
        /// Adds an item to TFS.  The pend action will be sent to TFS in a batch when the 
        /// Flush operation is performed.
        /// </summary>
        /// <param name="path">The TFS or local workspace path of the item to add to TFS.</param>
        /// <param name="downloadItem">The item to be downloaded from source system</param>
        public void PendAdd(string path, IMigrationItem downloadItem)
        {
            addSingleItem(path, downloadItem, ChangeAction.Add);
        }

        /// <summary>
        /// Deletes an item from TFS.  The pend action will be sent to TFS in a batch when the 
        /// Flush operation is performed.
        /// </summary>
        /// <param name="path">The TFS or local workspace path of the item to delete from TFS.</param>
        public void PendDelete(string path)
        {
            addSingleItem(path, null, ChangeAction.Delete);
        }

        private void addSingleItem(string path, IMigrationItem downloadItem, ChangeAction action)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentNullException("path");
            }

            string itemName;
            if (tryGetAsServerPath(path, out itemName))
            {
                BatchedItem item = new BatchedItem(itemName, action, downloadItem);
                addServerPathsToList(item);
            }
        }

        /// <summary>
        /// Edits an item in TFS.  The pend action will be sent to TFS in a batch when the 
        /// Flush operation is performed.
        /// </summary>
        /// <param name="path">The TFS or local workspace path of the item to edit in TFS.</param>
        /// <param name="downloadItem">The item to be downloaded from source system</param>
        public void PendEdit(string path, IMigrationItem downloadItem)
        {
            addSingleItem(path, downloadItem, ChangeAction.Edit);
        }

        /// <summary>
        /// Undeletes an item in TFS.  The pend action will be sent to TFS in a batch when the 
        /// Flush operation is performed.
        /// </summary>
        /// <param name="source">The source path of the item to undelete in TFS.</param>
        /// <param name="target">The target path of the item to undelete in TFS.</param>
        /// <param name="version">The version in which the item was deleted</param>
        public void PendUndelete(string source, string target, string deletedVersion)
        {
            string sourceItem;
            string targetItem;

            if (tryGetAsServerPath(source, out sourceItem) && tryGetAsServerPath(target, out targetItem))
            {
                BatchedItem undeleteItem = new BatchedItem(sourceItem, targetItem, deletedVersion);
                addServerPathsToList(undeleteItem);
            }
        }

        /// <summary>
        /// Renames an item in TFS.  The pend action will be sent to TFS in a batch when the 
        /// Flush operation is performed.
        /// </summary>
        /// <param name="source">The TFS or local workspace path of the source item of the rename operation.</param>
        /// <param name="target">The TFS or local workspace path of the target item of the rename operation.</param>
        public void PendRename(string source, string target)
        {
            string leftItem;
            string rightItem;

            if (tryGetAsServerPath(source, out leftItem) && tryGetAsServerPath(target, out rightItem))
            {
                BatchedItem renameItem = new BatchedItem(leftItem, rightItem, ChangeAction.Rename);

                addServerPathsToList(renameItem);
            }
        }

        /// <summary>
        /// Branches an item in TFS.  The pend action will be sent to TFS in a batch when the 
        /// Flush operation is performed.
        /// </summary>
        /// <param name="source">The TFS or local workspace path of the source item of the branch operation.</param>
        /// <param name="target">The TFS or local workspace path of the target item of the branch operation.</param>
        /// <param name="version">The branch from version</param>
        public void PendBranch(string source, string target, string version)
        {
            string sourceItem;
            string targetItem;

            if (tryGetAsServerPath(source, out sourceItem) && tryGetAsServerPath(target, out targetItem))
            {
                BatchedItem item = new BatchedItem(sourceItem, targetItem, RecursionType.Full, version);
                addServerPathsToList(item);
            }
        }


        /// <summary>
        /// Merges an item in TFS.  The pend action will be sent to TFS in a batch when the 
        /// Flush operation is performed.
        /// </summary>
        /// <param name="recurse">Recursive type of the merge operation</param>
        /// <param name="source">The TFS or local workspace path of the source item of the merge operation.</param>
        /// <param name="target">The TFS or local workspace path of the target item of the merge operation.</param>
        /// <param name="mergeVersionFrom">The starting version of the merge.</param>
        /// <param name="mergeVersionTo">The end version of the merge.</param>
        /// <param name="downloadItem">The item to be downloaded from source system</param>
        public void PendMerge(string source, 
            string target, 
            RecursionType recurse,
            string mergeVersionFrom,
            string mergeVersionTo,
            IMigrationItem downloadItem)
        {
            string sourceItem;
            string targetItem;

            if (tryGetAsServerPath(source, out sourceItem) && tryGetAsServerPath(target, out targetItem))
            {
                BatchedItem bmi = new BatchedItem(sourceItem, targetItem, recurse, mergeVersionFrom, mergeVersionTo, downloadItem);
                addServerPathsToList(bmi);
            }
        }

        private void addServerPathsToList(BatchedItem item)
        {
            /* -1 is infinite lock - flushing causes round-trips 
             * to the server so it could take a while
             */
            m_flushLock.AcquireReaderLock(-1);

            try
            {
                m_changeOpt.Add(item);
            }
            finally
            {
                m_flushLock.ReleaseReaderLock();
            }
        }

        /// <summary>
        /// Takes a local or server path and sets the out parameter to the TFS server path 
        /// (converting the local path to a server path) if, and only if, the resulting server
        /// path is also mapped in the workspace.  If the input path is either not a mapped path
        /// or is not a valid server path the out parameter is set to null and false is returned.
        /// </summary>
        /// <param name="path">A workspace local path or a TFS server path</param>
        /// <param name="serverPath">A TFS server path of the input item if the input item is mapped in the current workspace</param>
        /// <returns>True if a mapped server path was found, false otherwise.</returns>
        private bool tryGetAsServerPath(string path, out string serverPath)
        {
            serverPath = null;

            try
            {
                if (VersionControlPath.IsServerItem(path))
                {
                    if (m_workspace.IsServerPathMapped(path))
                    {
                        serverPath = path;
                    }
                }
                else
                {
                    serverPath = m_workspace.TryGetServerItemForLocalItem(path);
                }
            }
            catch (ArgumentException ex)
            {
                // Path is invalid
                OnBatchedItemWarning(new BatchedItem(path, ChangeAction.Unknown), ex.Message);
            }

            return serverPath != null;
        }

        /// <summary>
        /// Batches the queued operations and submits them to the TFS server.  The operations are submitted
        /// in the following order:
        /// 
        /// 1) Deletes
        /// 2) Adds
        /// 3) Undeletes
        /// 4) Edits
        /// 5) Branches
        /// 6) Merges
        /// 7) Renames
        /// 
        /// The order is important as it allows new items to be added in the namespace of deleted items.  It
        /// also ensures that edits are pended before renames are executed.
        /// </summary>
        public void Flush()
        {
            /* 
             * This will prevent the collections from being modified during the flush operation.
             * Since the Pend* methods will end up clearing the collections this is important.
             */

            m_flushLock.AcquireWriterLock(-1);

            try
            {
                m_items = m_changeOpt.Resolve();

                if (m_items.Count > 0)
                {
                    int currentPriority = m_items[0].Priority;

                    foreach (BatchedItem ci in m_items)
                    {
                        if (ci.Priority != currentPriority)
                        {
                            pendChanges();
                            currentPriority = ci.Priority;
                        }
                        if (!m_currentItems.ContainsKey(ci.Action))
                        {
                            m_currentItems.Add(ci.Action, new List<BatchedItem>());
                        }

                        m_currentItems[ci.Action].Add(ci);
                    }

                    pendChanges();

                    m_changeOpt.Clear();

                }
            }
            finally
            {
                m_flushLock.ReleaseWriterLock();
            }
        }

        void pendChanges()
        {
            pendUndeletes();
            pendBranches();
            pendDeletes();
            pendAdds();
            pendEdits();
            // Try add again for those edits without the original version on Tfs system
            pendAdds();
            pendRenames();
            pendMerges();

            m_currentItems.Clear();
        }

        private void pendMerges()
        {
            List<BatchedItem> batchedItems = getCurrent(ChangeAction.Merge);
            foreach (BatchedItem mergeItem in batchedItems)
            {
                GetStatus stat = null;

                setLocalVersions(new BatchedItem[] { mergeItem }, InvalidateAcceptMineOnMissingItem, RecursionType.None);

                mergeItem.DownloadItem.Download(m_workspace.GetLocalItemForServerItem(mergeItem.Target));

                VersionSpec mergeVersionFrom = new ChangesetVersionSpec(int.Parse(mergeItem.Version, CultureInfo.InvariantCulture));
                VersionSpec mergeVersionTo = new ChangesetVersionSpec(int.Parse(mergeItem.MergeVersionTo, CultureInfo.InvariantCulture));
                
                try
                {
                    stat = m_workspace.Merge(mergeItem.Source, mergeItem.Target, mergeVersionFrom, mergeVersionTo,
                        LockLevel.None, mergeItem.Recursion, mergeItem.MergeOption);
                }
                catch (NoMergeRelationshipException)
                {
                    stat = m_workspace.Merge(mergeItem.Source, mergeItem.Target, mergeVersionFrom, mergeVersionTo,
                        LockLevel.None, mergeItem.Recursion, mergeItem.MergeOption | MergeOptions.Baseless);

                }
                catch (VersionControlException vce)
                {
                    OnMergeError(
                        mergeItem,
                        null,
                        vce);

                    continue;
                }

                // OnMergeError could throw a VersionControlException so we do not want this code
                // inside the try block.
                if (stat == null || !getStatusIndicatesSuccess(stat))
                {
                    OnMergeError(
                        mergeItem,
                        stat,
                        null);
                }
            }
        }

        private static bool getStatusIndicatesSuccess(GetStatus status)
        {
            // if we have performed at least one operation or is no action is needed (merge /discard)
            // and we don't have failure, conflicts, warnings (resolvable or not), then we are true, otherwise false.
            return ((status.NumOperations > 0) || status.NoActionNeeded) &&
                    (status.NumFailures == 0) &&
                    (status.NumConflicts == 0) &&
                    (status.NumWarnings == 0);
        }

        private void pendBranches()
        {
            List<BatchedItem> batchedItems = getCurrent(ChangeAction.Branch);
            // sort branch actions so that the parent items are always processed before child items.
            batchedItems.Sort(compareBatchedItemByTargetPathLength);
            foreach (BatchedItem branchPath in batchedItems)
            {
                try
                {
                    m_workspace.PendBranch(branchPath.Source, branchPath.Target,
                        new ChangesetVersionSpec(int.Parse(branchPath.Version, CultureInfo.InvariantCulture)));

                    // Branch is always recursive. So we need to undo all sub item's branches.
                    PendingChange[] pendingChanges = m_workspace.GetPendingChanges(branchPath.Target, RecursionType.Full);
                    List<PendingChange> subPendingBranch = new List<PendingChange>();
                    for (int i = 0; i < pendingChanges.Length; i++)
                    {
                        if (!VersionControlPath.Equals(pendingChanges[i].ServerItem, branchPath.Target))
                        {
                            subPendingBranch.Add(pendingChanges[i]);
                        }
                    }
                    if (subPendingBranch.Count > 0)
                    {
                        m_workspace.Undo(subPendingBranch.ToArray());
                    }
                }
                catch (VersionControlException vce)
                {
                    OnBatchedItemError(
                        branchPath,
                        vce);
                }
            }
        }

        /// <summary>
        /// compare source path length of two batcheditem. If x's source path length is smaller, x is less than y.
        /// </summary>
        /// <param name="x">batched item 1</param>
        /// <param name="y">batched item 2</param>
        /// <returns>positive value if x's source length is larger. negative value if y's source length is larger.
        /// 0 if length is equal or either one is null.</returns>
        private static int compareBatchedItemBySourcePathLength(BatchedItem x, BatchedItem y)
        {
            if ((x == null) || (y == null))
            {
                return 0;
            }

            return (x.Source.Length - y.Source.Length);
        }

        /// <summary>
        /// compare target path length of two batcheditem. If x's target path length is smaller, x is less than y.
        /// </summary>
        /// <param name="x">batched item 1</param>
        /// <param name="y">batched item 2</param>
        /// <returns>positive value if x's target length is larger. negative value if y's target length is larger.
        /// 0 if length is equal or either one is null.</returns>
        private static int compareBatchedItemByTargetPathLength(BatchedItem x, BatchedItem y)
        {
            if ((x == null) || (y == null))
            {
                return 0;
            }

            return (x.Target.Length - y.Target.Length);
        }

        void throwOnMissingItem(BatchedItem serverItem)
        {
            throw new MigrationException(
                string.Format(MigrationToolkitVCResources.Culture, 
                MigrationToolkitVCResources.TfsItemMissing, 
                serverItem.Target)
                );
        }

        void InvalidateAcceptMineOnMissingItem(BatchedItem serverItem)
        {
            serverItem.MergeOption = serverItem.MergeOption & (~MergeOptions.AlwaysAcceptMine);
        }

        void changeToAddOnMissingItem(BatchedItem serverItem)
        {
            BatchedItem changedItem = new BatchedItem(serverItem.Target, ChangeAction.Add, serverItem.DownloadItem);
            if ( !m_currentItems.ContainsKey(ChangeAction.Add))
            {
                m_currentItems.Add(ChangeAction.Add, new List<BatchedItem>());
            }
            m_currentItems[ChangeAction.Add].Add(changedItem);
            serverItem.Skip = true;
        }

        private void pendRenames()
        {
            List<BatchedItem> batchedItems = getCurrent(ChangeAction.Rename);
            // sort rename actions so that the child items are always processed before parent items.
            batchedItems.Sort(compareBatchedItemBySourcePathLength);
            batchedItems.Reverse();
            // batch the set local versions in case there are many renames
            foreach (BatchedItem[] renamePaths in chunkCollection(batchedItems))
            {
                setLocalVersions(renamePaths, throwOnMissingItem, RecursionType.Full, true);
            }

            foreach (BatchedItem renamePath in batchedItems)
            {
                bool tryAgain = false;
                bool hasConflictedAdd = false;
                try
                {
                    m_workspace.PendRename(renamePath.Source, renamePath.Target, LockLevel.None, 
                        true, false);
                }
                catch (SystemException)
                {
                    // For case-only rename of folder, a nonfatal error of SystemException will be thrown. 
                    // But the rename is actually pended successfully. We should continue in this situation.
                    bool renamePended = false;
                    PendingChange[] pendingChanges = m_workspace.GetPendingChanges(renamePath.Target, RecursionType.None, false);
                    for (int i = 0; i < pendingChanges.Length; i++)
                    {
                        if ((pendingChanges[i].ChangeType & ChangeType.Rename) == ChangeType.Rename)
                        {
                            renamePended = true;
                            break;
                        }
                    }
                    if (!renamePended)
                    {
                        throw;
                    }
                }
                catch (ChangeAlreadyPendingException)
                {
                    tryAgain = true;
                    PendingChange[] pendingChanges = m_workspace.GetPendingChanges(renamePath.Target);
                    foreach (PendingChange change in pendingChanges)
                    {
                        if ((change.ChangeType & ChangeType.Branch) == ChangeType.Branch)
                        {
                            m_workspace.Undo(renamePath.Target);
                        }
                    }
                }
                catch (VersionControlException vce)
                {
                    OnBatchedItemError(
                        renamePath,
                        vce);
                }

                if (tryAgain)
                {
                    try
                    {
                        m_workspace.PendRename(renamePath.Source, renamePath.Target, LockLevel.None, true, false);
                    }
                    catch (SystemException)
                    {
                        // For case-only rename of folder, a nonfatal error of SystemException will be thrown. 
                        // But the rename is actually pended successfully. We should continue in this situation.
                        bool renamePended = false;
                        PendingChange[] pendingChanges = m_workspace.GetPendingChanges(renamePath.Target, RecursionType.None, false);
                        for (int i = 0; i < pendingChanges.Length; i++)
                        {
                            if ((pendingChanges[i].ChangeType & ChangeType.Rename) == ChangeType.Rename)
                            {
                                renamePended = true;
                                break;
                            }
                        }
                        if (!renamePended)
                        {
                            throw;
                        }
                    }
                    catch (VersionControlException vce)
                    {
                        OnBatchedItemError(
                            renamePath,
                            vce);
                    }
                }

            }
        }

        private void pendEdits()
        {
            List<BatchedItem> batchedItems = getCurrent(ChangeAction.Edit);
            foreach (BatchedItem[] serverPaths in chunkCollection(batchedItems))
            {
                bool addItemNotFound = MigrationConfiguration.Current.GetValue<bool>("AddItemNotFound", true);
                if (addItemNotFound)
                {
                    setLocalVersions(serverPaths, changeToAddOnMissingItem, RecursionType.None);
                }
                else
                {
                    setLocalVersions(serverPaths, throwOnMissingItem, RecursionType.None);
                }

                downloadItems(serverPaths);

                string[] items = createItemArray(serverPaths);

                if (items.Length == 0)
                {
                    return;
                }

                bool tryAgain = true;
                try
                {
                    m_workspace.PendEdit(items);
                    tryAgain = false;
                }
                catch (VersionControlException vce)
                {
                    OnBatchedItemError(
                        null,
                        vce);
                }

                if (tryAgain)
                {
                    int index = 0;
                    foreach (string item in items)
                    {
                        try
                        {
                            m_workspace.PendEdit(item);
                        }
                        catch (VersionControlException vceInner)
                        {
                            OnBatchedItemError(
                                serverPaths[index],
                                vceInner);
                        }

                        index++;
                    }
                }
            }
        }

        private void downloadItems(BatchedItem[] itemsToBeDownloaded)
        {
            foreach (BatchedItem itemToBeDownloaded in itemsToBeDownloaded)
            {
                itemToBeDownloaded.DownloadItem.Download(m_workspace.GetLocalItemForServerItem(itemToBeDownloaded.Target));
            }            
        }

        private static string[] createItemArray(BatchedItem[] serverPaths)
        {
            string[] items = new string[serverPaths.Length];
            int counter = 0;
            
            foreach (BatchedItem serverItem in serverPaths)
            {
                if (!serverItem.Skip)
                {
                    items[counter++] = serverItem.Target;
                }
                else
                {
                    TraceManager.TraceInformation("Skipping item {0}", serverItem.Target);
                }
            }

            if (items.Length != counter)
            {
                Array.Resize<string>(ref items, counter);
            }

            return items;
        }

        private void pendDeletes()
        {
            List<BatchedItem> batchedItems = getCurrent(ChangeAction.Delete);

            foreach (BatchedItem[] serverPaths in chunkCollection(batchedItems))
            {
                setLocalVersions(serverPaths, skipMissingItem, RecursionType.None);

                string[] items = createItemArray(serverPaths);

                if (items.Length > 0)
                {

                    bool tryAgain = true;
                    try
                    {
                        m_workspace.PendDelete(items);
                        tryAgain = false;
                    }
                    catch (VersionControlException vce)
                    {
                        OnBatchedItemError(
                            null,
                            vce);
                    }

                    if (tryAgain)
                    {
                        int index = 0;

                        foreach (string item in items)
                        {
                            try
                            {
                                m_workspace.PendDelete(item);
                            }
                            catch (VersionControlException vceInner)
                            {
                                OnBatchedItemError(
                                    serverPaths[index],
                                    vceInner);
                            }

                            index++;
                        }
                    }
                }
            }
        }

        private List<BatchedItem> getCurrent(ChangeAction changeAction)
        {
            if (m_currentItems.ContainsKey(changeAction))
            {
                return m_currentItems[changeAction];
            }

            List<BatchedItem> newList = new List<BatchedItem>(0);
            m_currentItems.Add(changeAction, newList);

            return newList;
        }

        private void skipMissingItem(BatchedItem serverPath)
        {
            serverPath.Skip = true;
            OnBatchedItemWarning(serverPath, string.Format(MigrationToolkitVCResources.Culture, MigrationToolkitVCResources.SkipMissingItem, serverPath.Target,
                serverPath.Action.ToString()));
        }

        private void pendAdds()
        {
            List<BatchedItem> batchedItems = getCurrent(ChangeAction.Add);

            foreach (BatchedItem[] serverPaths in chunkCollection(batchedItems))
            {
                string[] items = createItemArray(serverPaths);

                downloadItems(serverPaths);
                
                bool tryAgain = true;

                try
                {
                    m_workspace.PendAdd(items);
                    tryAgain = false;
                }
                catch (Exception e)
                {
                    if (e is VersionControlException || e is MigrationException) 
                    {
                        // If we catch ItemNotMappedException, then item to be added is mapped but its parent is not mapped.
                        // We will add its parent to the mapping and try pendAdd again.
                        if ( !(e is ItemNotMappedException))
                        {
                            OnBatchedItemError(
                                new BatchedItem("Unknown", ChangeAction.Add),
                                e);
                        }
                    }
                    else
                    {
                        throw;
                    }
                }

                if (tryAgain)
                {
                    foreach (string item in items)
                    {
                        int index = 0;
                        try
                        {
                            if (!m_workspace.IsServerPathMapped(VersionControlPath.GetFolderName(item)))
                            {
                                DirectoryInfo parentLocalDir = Directory.GetParent(m_workspace.GetLocalItemForServerItem(item));
                                m_workspace.Map(VersionControlPath.GetFolderName(item), parentLocalDir.FullName);
                            }
                            m_workspace.PendAdd(item);
                        }
                        catch (Exception eInner)
                        {
                            if (eInner is VersionControlException || eInner is MigrationException)
                            {
                                OnBatchedItemError(
                                    serverPaths[index],
                                    eInner);
                            }
                            else
                            {
                                throw;
                            }
                        }

                        index++;
                    }
                }
            }
        }

        private static ItemSpec[] getItemSpecsFromServerPath(BatchedItem[] serverPaths)
        {
            ItemSpec[] specs = new ItemSpec[serverPaths.Length];

            int current = 0;
            int i = 0;
            for (; i < serverPaths.Length; i++)
            {
                if (!serverPaths[i].Skip)
                {
                    specs[current++] = new ItemSpec(serverPaths[i].Target, RecursionType.None);
                }
            }

            if (current != specs.Length)
            {
                Array.Resize<ItemSpec>(ref specs, current);
            }

            return specs;
        }

        private void pendUndeletes()
        {
            List<BatchedItem> batchedItems = getCurrent(ChangeAction.Undelete);

            if (batchedItems.Count > 0)
            {
                foreach (BatchedItem batchedItem in batchedItems)
                {
                    // Get the deletionId of the item to be undeleted.
                    ItemSpec[] itemSpec = { new ItemSpec(batchedItem.Source, RecursionType.Full) };
                    ItemSet[] sets = m_workspace.VersionControlServer.GetItems(itemSpec, VersionSpec.Latest, DeletedState.Deleted, ItemType.Any);
                    Debug.Assert(sets.Length == 1);
                    if (sets.Length != 1)
                    {
                        throw new MigrationException(
                            string.Format(
                                MigrationToolkitVCResources.Culture,
                                MigrationToolkitVCResources.UnexpectedNumberOfItems,
                                1,
                                sets.Length));
                    }
                    if (sets[0].Items.Length == 0)
                    {
                        OnBatchedItemError(
                            batchedItem,
                            new ItemNotFoundException(
                                string.Format(
                                    MigrationToolkitVCResources.Culture,
                                    MigrationToolkitVCResources.DeletedItemNotFound,
                                    batchedItem.Target)));
                    }
                    else
                    {
                        Item deletedItem = sets[0].Items[0];
                        bool deletedVersionfound = false;

                        if (sets[0].Items.Length > 1)
                        {
                            int deletedChangeset = int.Parse(batchedItem.Version, CultureInfo.InvariantCulture);
                            foreach (Item item in sets[0].Items)
                            {
                                if (item.ChangesetId == deletedChangeset)
                                {
                                    deletedItem = item;
                                    deletedVersionfound = true;
                                    break;
                                }
                            }
                            if (!deletedVersionfound)
                            {
                                OnBatchedItemWarning(batchedItem,
                                    string.Format(MigrationToolkitVCResources.Culture,
                                    MigrationToolkitVCResources.DeletedVersionNotFound, 
                                    batchedItem.Target));                                    
                            }
                        }
                        using (UpdateLocalVersionQueue q = new UpdateLocalVersionQueue(m_workspace))
                        {
                            //Update the item version with the deleted changeset.
                            q.QueueUpdate(deletedItem.ItemId, m_workspace.GetLocalItemForServerItem(deletedItem.ServerItem),
                                deletedItem.ChangesetId);
                            q.Flush();
                        }
                        try
                        {
                            m_workspace.PendUndelete(batchedItem.Source, deletedItem.DeletionId, batchedItem.Target);
                        }
                        catch (VersionControlException vceInner)
                        {
                            OnBatchedItemError(
                                batchedItem,
                                vceInner);
                        }
                    }
                }
            }
        }

        internal List<T[]> chunkCollection<T>(List<T> paths)
        {
            List<T[]> batches = new List<T[]>();
            int length = paths.Count;

            int currentIndex = 0;
            int remaining = length - currentIndex;

            while (remaining > 0)
            {
                int currentBatchSize = (remaining < m_batchSize) ? remaining : m_batchSize;
                batches.Add(
                    paths.GetRange(
                        currentIndex,
                        currentBatchSize
                    ).ToArray()
                );

                currentIndex += currentBatchSize;
                remaining = length - currentIndex;
            }

            return batches;
        }

        delegate void onMissingItem(BatchedItem serverPath);

        private void setLocalVersions(BatchedItem[] serverPaths, onMissingItem missingItem, RecursionType recursionType)
        {
            setLocalVersions(serverPaths, missingItem, recursionType, false);
        }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1817:DoNotCallPropertiesThatCloneValuesInLoops")]
        private void setLocalVersions(BatchedItem[] serverPaths, onMissingItem missingItem, RecursionType recursionType, bool useSourcePath)
        {
            int count = serverPaths.Length;
            using (UpdateLocalVersionQueue q = new UpdateLocalVersionQueue(m_workspace))
            {
                ItemSpec[] itemSpecs = new ItemSpec[count];

                for (int i = 0; i < count; i++)
                {
                    string serverPath = (useSourcePath) ? serverPaths[i].Source : serverPaths[i].Target;
                    itemSpecs[i] = new ItemSpec(serverPath, recursionType);
                }

                // Perform a fake get on the items
                ItemSet[] sets = m_workspace.VersionControlServer.GetItems(itemSpecs, VersionSpec.Latest, DeletedState.NonDeleted, ItemType.Any);
                Debug.Assert(sets.Length == count);

                if (sets.Length != count)
                {
                    throw new MigrationException(
                        string.Format(
                            MigrationToolkitVCResources.Culture,
                            MigrationToolkitVCResources.UnexpectedNumberOfItems,
                            count, 
                            sets.Length));
                }

                bool succeeded = false;
                int counter = 0;

                while (!succeeded)
                {
                    try
                    {
                        for (int i = 0; i < count; i++)
                        {
                            Item[] items = sets[i].Items;
                            if (items.Length > 0)
                            {
                                foreach (Item item in items)
                                {
                                    q.QueueUpdate(item.ItemId, m_workspace.GetLocalItemForServerItem(item.ServerItem), item.ChangesetId);
                                }
                            }
                            else
                            {
                                /* if we made it here the item does not exist in TFS (as a non-deleted item)
                                 * but we may have an undelete or rename or branch pended in the local workspace in which case
                                 * the item is not really missing. */
                                string itemName = itemSpecs[i].Item;

                                bool hasPendingRenameBranchOrUndelete = false;
                                PendingChange[] pendingChanges = m_workspace.GetPendingChanges(itemName, RecursionType.None, false);
                                if (pendingChanges.Length != 0)
                                {
                                    foreach (PendingChange pendingChange in pendingChanges)
                                    {
                                        if (VersionControlPath.Compare(pendingChange.ServerItem, itemName) == 0)
                                        {
                                            if ((pendingChange.ChangeType & ChangeType.Undelete) == ChangeType.Undelete)
                                            {
                                                //Roll back the local workspace version to the undeleted state, 
                                                //so that the new change can be pended.
                                                q.QueueUpdate(pendingChange.ItemId, m_workspace.GetLocalItemForServerItem(itemName),
                                                    pendingChange.Version);
                                            }
                                            hasPendingRenameBranchOrUndelete = true;
                                            break;
                                        }
                                    }
                                }

                                if (!hasPendingRenameBranchOrUndelete)
                                {                                    
                                    // Undelete and rename is always recursive, but the pending change cannot be queried on child item. 
                                    // In this case, the child item is not really missing.
                                    string parentItemName = VersionControlPath.GetFolderName(itemName);
                                    bool hasPendingParentUndeleteOrRename = false;
                                    while ((!hasPendingParentUndeleteOrRename) &&
                                        (VersionControlPath.Compare(parentItemName, VersionControlPath.RootFolder) != 0))
                                    {
                                        pendingChanges = m_workspace.GetPendingChanges(parentItemName, RecursionType.None, false);
                                        foreach (PendingChange change in pendingChanges)
                                        {
                                            if ((change.ChangeType & ChangeType.Undelete) == ChangeType.Undelete) 
                                                
                                            {
                                                hasPendingParentUndeleteOrRename = true;
                                                //Roll back the local workspace version to the undeleted state, 
                                                //so that the new change can be pended.
                                                ItemSet set = m_workspace.VersionControlServer.GetItems(itemSpecs[i], VersionSpec.Latest,
                                                    DeletedState.Deleted, ItemType.Any, false);
                                                q.QueueUpdate(set.Items[0].ItemId, m_workspace.GetLocalItemForServerItem(itemName),
                                                    change.Version);
                                                break;
                                            }
                                            if ((change.ChangeType & ChangeType.Rename) == ChangeType.Rename)
                                            {
                                                hasPendingParentUndeleteOrRename = true;
                                                break;
                                            }
                                        }
                                        parentItemName = VersionControlPath.GetFolderName(parentItemName);
                                    }
                                    if (!hasPendingParentUndeleteOrRename)
                                    {
                                        missingItem(serverPaths[i]);
                                    }
                                }
                            }
                        }

                        q.Flush();
                        succeeded = true;
                    }
                    catch (RepositoryNotFoundException)
                    {
                        counter++;
                        if (counter > m_retryLimit)
                        {
                            throw;
                        }

                        for (int i = 0; i < m_secondsToWait; i++)
                        {
                            System.Threading.Thread.Sleep(1000);
                        };
                    }
                }
            }
        }

        private void OnBatchedItemError(BatchedItem item, Exception exception)
        {
            if (BatchedItemError != null)
            {
                BatchedItemError(this, new BatchedItemEventArgs(item, exception, exception.Message));
            }
            else
            {
                Debug.Fail("An error occurred but there was no listener");

                if (exception != null)
                {
                    throw exception;
                }
            }
        }

        private void OnBatchedItemWarning(BatchedItem item, string message)
        {
            if (BatchedItemWarning != null)
            {
                BatchedItemWarning(this, new BatchedItemEventArgs(item, null, message));
            }
        }


        private void OnMergeError(BatchedItem item, GetStatus status, Exception exception)
        {
            if (MergeError != null)
            {
                MergeError(this, new BatchedMergeErrorEventArgs(item, status, exception));
            }
            else
            {
                Debug.Fail("An error occurred but there was no listener");

                if (exception != null)
                {
                    throw exception;
                }
            }
        }

        Workspace m_workspace;

        ChangeOptimizer m_changeOpt = new ChangeOptimizer();

        Dictionary<ChangeAction, List<BatchedItem>> m_currentItems = new Dictionary<ChangeAction,List<BatchedItem>>();
        ReadOnlyCollection<BatchedItem> m_items;

        // used to block write access to the collections by other threads when flushing
        ReaderWriterLock m_flushLock = new ReaderWriterLock();

        int m_batchSize;
        int m_retryLimit;
        int m_secondsToWait;
    }
}
